document.body.appendChild(el)
elem.style.display = "none"
어떤 순서로 실행될까? Element가 붙고나서 잠깐이라도 보여지지 않을까? Event Loop을 잘 이해한다면 그런 걱정은 하지 않아도 된다.
Main Thread에서 모든 것은 결정적으로 처리된다.
while (true); // Main thread blocking, render blocking
function loop() {
setTimeout(loop, 0) // queued for next loop, not render blocking
}
loop()
ℹ️ Event loop의 rendering 과정에서는 대략 Style, Layout, Paint순으로 렌더링을 시도한다.
브라우저가 항상 render 하는 것은 아니다. 하지만 render시점에 callback 함수를 호출하고 싶으면 다음을 이용할 수 있다.
requestAnimationFrame() // rAF
rAF
는 rendering바로 직전에 실행되는 것이 브라우저 표준 구현방식이다.
Event loop의 매 시작에는 render step이 있다. Render step이 끝난 후 메인 쓰레드의 남는 자원은 task queue의 event처리에 쓰인다.
그 말인 즉슨, task event의 경우 event loop의 어느 시점에 처리될 지 정확히 알 수 없다는 말이 된다. 그렇기 때문에 애니메이션등의 그려지는 동작의 통일성을 보장하려면 setTimeout
을 쓰면안된다.
setTimout
은 단순히 task를 대기열에 넣고 event loop이 여유가 있을 때마다 실행한다. 렌더링 과정이 한번 일어나기 전에 여러번 호출 될 수 있다는 얘기이다. 위치 값 갱신 같은 작업이 그려지기 전에 두번 이상 발생하면 정확한 애니메이션을 그릴 수 없다. 또한 만약 작업이 오래 걸리는 경우 병목이 생기면서 다음 event loop를 지연시킬 수도 있기 때문에 버벅임이 발생할 가능성도 있다.
같은 이유로 render관련된 서버 동작이 있을 경우에 setTimeout
대신 rAF
을 이용하면 불필요한 호출을 줄일 수 있을 것이다.
다음 예제를 보자:
클릭시에 상자를 1000px
의 위치로 이동시켰다가 500px
으로 이동시키는 코드이다.
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-out';
box.style.transform = 'translateX(500px)';
});
하지만 이 상태로 실행하면 상자는 0의 위치에서부터 500으로 이동한다. 왜 그럴까?
아까 말했듯 rendering step이 발생하지 않은 상태에서 실행이 됐기 때문에 마지막 값으로 할당이 된 것이다.
하지만 여기서 rAF
callback에서 값을 바꾸더라도 애니메이션이 의도한대로 보여지지 않는다. 아까 말했듯 rAF
는 painting이전에 실행되기 때문이다.
⚠️ Safari는 rAF
가 painting 이후에 호출됐던 적이 있다.
탄생 배경? 아래의 코드는 몇개의 Event를 만들어낼까?
for (let i = 0; i < 100; i++) {
const span = document.createElement("span")
document.body.appendChild(span)
span.textContent = "Hello"
}
Loop전체에 해당하는 1개일까? 답은 200개이다. textNode가 span에 삽입되는 과정이 100개 추가되어서 그렇다.
DOM 조작은 이처럼 성능에 문제가 되었고, 개발자들은 이 과정을 batch로 처리하고 싶었다.
그래서 MutationObserver
가 나오고 Microtask가 나왔다.
Microtask는 일반적으로 JavaScript가 모두 실행되고 JavaScript Stack이 비었을 때 실행된다.
Promise.resolve().then(() => console.log("World"))
console.log("Hello")
아래 로그를 다 실행하면 위의 callback이 실행된다.
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 1"))
console.log("Listener 1")
})
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 2"))
cosnole.log("Listener 2")
})
button.click()
: Listener 1 -> Listener 2 -> Microtask 1 -> Microtask 2const nextClick = new Promise(resolve => {
link.addEventListener("click", resolve, { once: true })
})
nextClick.then(event => {
event.preventDefault()
})
link.click()
예상 답: click이벤트에는 아무것도 없다. task queue를 계속 차지하고 있어서 microtask인 promise가 eventListener를 붙여주지 못함.